Skip to content

RAG 离线索引阶段的去重

在离线索引阶段进行去重非常关键,因为重复或近似重复的文本块会降低检索质量:它们会使结果充斥冗余内容,扭曲相关性评分,并浪费向量数据库的存储容量。


重复产生的原因

  • 文档更新后重新导入(只有部分文本块发生变化)
  • 重叠的分块策略(如高重叠的滑动窗口)
  • 多个数据源管道写入同一向量数据库
  • 文档间的镜像内容(模板、免责声明、共享章节等)

去重策略

1. 精确去重(嵌入前)

在调用嵌入模型之前进行 —— 成本最低。

哈希指纹法示例:

python
import hashlib

def chunk_fingerprint(text: str) -> str:
    normalized = " ".join(text.lower().split())  # 标准化空格和大小写
    return hashlib.sha256(normalized.encode()).hexdigest()

# 插入时:
if fingerprint in seen_hashes:
    skip_chunk()
else:
    seen_hashes.add(fingerprint)
    embed_and_insert(chunk)

可以将哈希作为 元数据字段 存入向量数据库,以便无需全量扫描即可查询。


2. 近似重复检测(嵌入前或嵌入后)

用于语义相同但表面略有差异的文本块(空格、标点、版本差异等)。

MinHash / LSH(嵌入前)示例:

python
from datasketch import MinHash, MinHashLSH

lsh = MinHashLSH(threshold=0.85, num_perm=128)

def get_minhash(text):
    m = MinHash(num_perm=128)
    for word in text.lower().split():
        m.update(word.encode())
    return m

# 插入时:
m = get_minhash(chunk_text)
if lsh.query(m):           # 找到近似重复
    skip_or_merge(chunk)
else:
    lsh.insert(chunk_id, m)
    embed_and_insert(chunk)

基于嵌入向量的余弦相似度(嵌入后):

python
# 插入前查询向量数据库的最近邻
results = vector_db.query(embedding, top_k=3)
if results and results[0].score > 0.97:   # 阈值可调
    skip_chunk()
else:
    vector_db.insert(chunk_id, embedding, metadata)

⚠️ 在大规模场景下成本较高 —— 仅在 LSH 预筛选不足时使用。


3. 文档级版本管理 & Upsert

在重新导入更新文档时,避免累积过时的文本块:

python
# 给每个文本块打上文档 ID + 版本/哈希
metadata = {
    "doc_id": "policy_v3.pdf",
    "doc_hash": document_hash,
    "chunk_index": i,
}

# 重新导入时:先删除该文档旧的文本块,再插入新块
vector_db.delete(filter={"doc_id": "policy_v3.pdf"})
vector_db.insert_batch(new_chunks)

相比直接更新/Upsert,这种方式更优,因为不同版本的文本块数量和边界可能不同。


4. 文本块级去重策略

并非所有重复都应删除 —— 有时 来源信息很重要

情况推荐操作
完全相同,来源相同丢弃 —— 纯重复
完全相同,来源不同保留一份,合并来源元数据
近似重复,语境不同保留两份,标注相似度
模板内容(页脚、免责声明等)全局丢弃,通过黑名单过滤

5. 模板/结构噪声移除

在文本块到达去重阶段前先剔除重复、无信息量的内容:

python
BOILERPLATE_PATTERNS = [
    r"page \d+ of \d+",
    r"confidential – do not distribute",
    r"all rights reserved",
]

def is_boilerplate(text: str) -> bool:
    text_lower = text.lower().strip()
    if len(text_lower) < 40:          # 太短无意义
        return True
    return any(re.search(p, text_lower) for p in BOILERPLATE_PATTERNS)

推荐管道架构

原始文档


[分块] ──► [模板过滤]


              [精确哈希检查] ── 重复 → 跳过
                     │ 唯一

              [MinHash/LSH 检查] ── 近似重复 → 跳过或合并
                     │ 唯一

              [嵌入模型]


              [可选:高价值内容余弦相似度检查]


              [向量数据库插入并存储元数据]

必须存储的关键元数据字段

json
{
  "chunk_id":       "uuid-v4",
  "doc_id":         "source-document-identifier",
  "doc_hash":       "sha256 of full document",
  "chunk_hash":     "sha256 of normalized chunk text",
  "chunk_index":    2,
  "ingested_at":    "2026-05-07T10:00:00Z",
  "source_path":    "s3://bucket/policy_v3.pdf"
}

这些元数据可支持 选择性重建索引、审计跟踪,以及在检索阶段进行运行时去重检查。


阈值调优建议

信号保守(低噪声)激进(高去重)
余弦相似度阈值0.990.93
MinHash Jaccard 阈值0.900.80
最小文本块长度(字符数)100200

建议先采用保守策略,根据检索质量指标(MRR、nDCG)在保留集上进行调优。